package com.only.fx.common.component.skin;

import com.sun.javafx.scene.traversal.ParentTraversalEngine;
import javafx.animation.Animation.Status;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.DoublePropertyBase;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventDispatcher;
import javafx.event.EventHandler;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Orientation;
import javafx.scene.AccessibleAttribute;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.input.TouchEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import com.sun.javafx.util.Utils;
import com.sun.javafx.scene.control.behavior.ScrollPaneBehavior;
import com.sun.javafx.scene.control.skin.BehaviorSkinBase;
import com.sun.javafx.scene.traversal.TraverseListener;
import static com.sun.javafx.scene.control.skin.Utils.*;
import javafx.geometry.Insets;

public class OnlyScrollPaneSkin extends BehaviorSkinBase<ScrollPane, ScrollPaneBehavior> implements TraverseListener {

	/***************************************************************************
	 * * UI Subcomponents * *
	 **************************************************************************/

	private static final double DEFAULT_PREF_SIZE = 100.0;

	private static final double DEFAULT_MIN_SIZE = 36.0;

	private static final double DEFAULT_SB_BREADTH = 12.0;
	private static final double DEFAULT_EMBEDDED_SB_BREADTH = 8.0;

	private static final double PAN_THRESHOLD = 0.5;

	// state from the control

	private Node scrollNode;

	private double nodeWidth;
	private double nodeHeight;
	private boolean nodeSizeInvalid = true;

	private double posX;
	private double posY;

	// working state

	private boolean hsbvis;
	private boolean vsbvis;
	private double hsbHeight;
	private double vsbWidth;

	// substructure

	private StackPane viewRect;
	private StackPane viewContent;
	private double contentWidth;
	private double contentHeight;
	private StackPane corner;
	protected ScrollBar hsb;
	protected ScrollBar vsb;

	double pressX;
	double pressY;
	double ohvalue;
	double ovvalue;
	private Cursor saveCursor = null;
	private boolean dragDetected = false;
	private boolean touchDetected = false;
	private boolean mouseDown = false;

	Rectangle clipRect;

	/***************************************************************************
	 * * Constructors * *
	 **************************************************************************/

	public OnlyScrollPaneSkin(final ScrollPane scrollpane) {
		super(scrollpane, new ScrollPaneBehavior(scrollpane));
		initialize();
		// Register listeners
		registerChangeListener(scrollpane.contentProperty(), "NODE");
		registerChangeListener(scrollpane.fitToWidthProperty(), "FIT_TO_WIDTH");
		registerChangeListener(scrollpane.fitToHeightProperty(), "FIT_TO_HEIGHT");
		registerChangeListener(scrollpane.hbarPolicyProperty(), "HBAR_POLICY");
		registerChangeListener(scrollpane.vbarPolicyProperty(), "VBAR_POLICY");
		registerChangeListener(scrollpane.hvalueProperty(), "HVALUE");
		registerChangeListener(scrollpane.hmaxProperty(), "HMAX");
		registerChangeListener(scrollpane.hminProperty(), "HMIN");
		registerChangeListener(scrollpane.vvalueProperty(), "VVALUE");
		registerChangeListener(scrollpane.vmaxProperty(), "VMAX");
		registerChangeListener(scrollpane.vminProperty(), "VMIN");
		registerChangeListener(scrollpane.prefViewportWidthProperty(), "VIEWPORT_SIZE_HINT");
		registerChangeListener(scrollpane.prefViewportHeightProperty(), "VIEWPORT_SIZE_HINT");
		registerChangeListener(scrollpane.minViewportWidthProperty(), "VIEWPORT_SIZE_HINT");
		registerChangeListener(scrollpane.minViewportHeightProperty(), "VIEWPORT_SIZE_HINT");
	}

	private final InvalidationListener nodeListener = new InvalidationListener() {
		@Override
		public void invalidated(Observable valueModel) {
			if (!nodeSizeInvalid) {
				final Bounds scrollNodeBounds = scrollNode.getLayoutBounds();
				final double scrollNodeWidth = scrollNodeBounds.getWidth();
				final double scrollNodeHeight = scrollNodeBounds.getHeight();

				/*
				 ** if the new size causes scrollbar visibility to change, then
				 * need to relayout we also need to correct the thumb size when
				 * the scrollnode's size changes
				 */
				if (vsbvis != determineVerticalSBVisible() || hsbvis != determineHorizontalSBVisible() ||
						(scrollNodeWidth != 0.0 && nodeWidth != scrollNodeWidth) ||
						(scrollNodeHeight != 0.0 && nodeHeight != scrollNodeHeight)) {
					getSkinnable().requestLayout();
				} else {
					/**
					 * we just need to update scrollbars based on new scrollNode
					 * size, but we don't do this while dragging, there's no
					 * need, and it jumps, as dragging updates the scrollbar
					 * too.
					 */
					if (!dragDetected) {
						updateVerticalSB();
						updateHorizontalSB();
					}
				}
			}
		}
	};

	/*
	 ** The content of the ScrollPane has just changed bounds, check scrollBar
	 * positions.
	 */
	private final ChangeListener<Bounds> boundsChangeListener = new ChangeListener<Bounds>() {
		@Override
		public void changed(ObservableValue<? extends Bounds> observable, Bounds oldBounds, Bounds newBounds) {

			/*
			 ** For a height change then we want to reduce viewport vertical
			 * jumping as much as possible. We set a new vsb value to try to
			 * keep the same content position at the top of the viewport
			 */
			double oldHeight = oldBounds.getHeight();
			double newHeight = newBounds.getHeight();
			if (oldHeight > 0 && oldHeight != newHeight) {
				double oldPositionY = (snapPosition(snappedTopInset() - posY / (vsb.getMax() - vsb.getMin()) * (oldHeight - contentHeight)));
				double newPositionY = (snapPosition(snappedTopInset() - posY / (vsb.getMax() - vsb.getMin()) * (newHeight - contentHeight)));

				double newValueY = (oldPositionY / newPositionY) * vsb.getValue();
				if (newValueY < 0.0) {
					vsb.setValue(0.0);
				} else if (newValueY < 1.0) {
					vsb.setValue(newValueY);
				} else if (newValueY > 1.0) {
					vsb.setValue(1.0);
				}
			}

			/*
			 ** For a width change then we want to reduce viewport horizontal
			 * jumping as much as possible. We set a new hsb value to try to
			 * keep the same content position to the left of the viewport
			 */
			double oldWidth = oldBounds.getWidth();
			double newWidth = newBounds.getWidth();
			if (oldWidth > 0 && oldWidth != newWidth) {
				double oldPositionX = (snapPosition(snappedLeftInset() - posX / (hsb.getMax() - hsb.getMin()) * (oldWidth - contentWidth)));
				double newPositionX = (snapPosition(snappedLeftInset() - posX / (hsb.getMax() - hsb.getMin()) * (newWidth - contentWidth)));

				double newValueX = (oldPositionX / newPositionX) * hsb.getValue();
				if (newValueX < 0.0) {
					hsb.setValue(0.0);
				} else if (newValueX < 1.0) {
					hsb.setValue(newValueX);
				} else if (newValueX > 1.0) {
					hsb.setValue(1.0);
				}
			}
		}
	};

	@SuppressWarnings("deprecation")
	private void initialize() {
		// requestLayout calls below should not trigger requestLayout above
		// ScrollPane
		// setManaged(false);

		ScrollPane control = getSkinnable();
		scrollNode = control.getContent();

		ParentTraversalEngine traversalEngine = new ParentTraversalEngine(getSkinnable());
		traversalEngine.addTraverseListener(this);
		getSkinnable().setImpl_traversalEngine(traversalEngine);

		if (scrollNode != null) {
			scrollNode.layoutBoundsProperty().addListener(nodeListener);
			scrollNode.layoutBoundsProperty().addListener(boundsChangeListener);
		}

		viewRect = new StackPane() {

			@Override
			protected void layoutChildren() {
				viewContent.resize(getWidth(), getHeight());
			}

		};
		// prevent requestLayout requests from within scrollNode from
		// percolating up
		viewRect.setManaged(false);
		viewRect.setCache(true);
		viewRect.getStyleClass().add("viewport");

		clipRect = new Rectangle();
		viewRect.setClip(clipRect);

		hsb = new ScrollBar();

		vsb = new ScrollBar();
		vsb.setOrientation(Orientation.VERTICAL);

		EventHandler<MouseEvent> barHandler = ev -> {
			getSkinnable().requestFocus();
		};

		hsb.addEventFilter(MouseEvent.MOUSE_PRESSED, barHandler);
		vsb.addEventFilter(MouseEvent.MOUSE_PRESSED, barHandler);

		corner = new StackPane();
		corner.getStyleClass().setAll("corner");

		viewContent = new StackPane() {
			@Override
			public void requestLayout() {
				// if scrollNode requested layout, will want to recompute
				nodeSizeInvalid = true;

				super.requestLayout(); // add as layout root for next layout
										// pass

				// Need to layout the ScrollPane as well in case scrollbars
				// appeared or disappeared.
				OnlyScrollPaneSkin.this.getSkinnable().requestLayout();
			}

			@Override
			protected void layoutChildren() {
				if (nodeSizeInvalid) {
					computeScrollNodeSize(getWidth(), getHeight());
				}
				if (scrollNode != null && scrollNode.isResizable()) {
					scrollNode.resize(snapSize(nodeWidth), snapSize(nodeHeight));
					if (vsbvis != determineVerticalSBVisible() || hsbvis != determineHorizontalSBVisible()) {
						getSkinnable().requestLayout();
					}
				}
				if (scrollNode != null) {
					scrollNode.relocate(0, 0);
				}
			}
		};
		viewRect.getChildren().add(viewContent);

		if (scrollNode != null) {
			viewContent.getChildren().add(scrollNode);
			viewRect.nodeOrientationProperty().bind(scrollNode.nodeOrientationProperty());
		}

		getChildren().clear();
		getChildren().addAll(viewRect, vsb, hsb, corner);

		/*
		 ** listeners, and assorted housekeeping
		 */
		InvalidationListener vsbListener = valueModel -> {
			if (!IS_TOUCH_SUPPORTED) {
				posY = Utils.clamp(getSkinnable().getVmin(), vsb.getValue(), getSkinnable().getVmax());
			} else {
				posY = vsb.getValue();
			}
			updatePosY();
		};
		vsb.valueProperty().addListener(vsbListener);

		InvalidationListener hsbListener = valueModel -> {
			if (!IS_TOUCH_SUPPORTED) {
				posX = Utils.clamp(getSkinnable().getHmin(), hsb.getValue(), getSkinnable().getHmax());
			} else {
				posX = hsb.getValue();
			}
			updatePosX();
		};
		hsb.valueProperty().addListener(hsbListener);

		viewRect.setOnMousePressed(e -> {
			mouseDown = true;
			if (IS_TOUCH_SUPPORTED) {
				startSBReleasedAnimation();
			}
			pressX = e.getX();
			pressY = e.getY();
			ohvalue = hsb.getValue();
			ovvalue = vsb.getValue();
		});

		viewRect.setOnDragDetected(e -> {
			if (IS_TOUCH_SUPPORTED) {
				startSBReleasedAnimation();
			}
			if (getSkinnable().isPannable()) {
				dragDetected = true;
				if (saveCursor == null) {
					saveCursor = getSkinnable().getCursor();
					if (saveCursor == null) {
						saveCursor = Cursor.DEFAULT;
					}
					getSkinnable().setCursor(Cursor.MOVE);
					getSkinnable().requestLayout();
				}
			}
		});

		viewRect.addEventFilter(MouseEvent.MOUSE_RELEASED, e -> {
			mouseDown = false;
			if (dragDetected == true) {
				if (saveCursor != null) {
					getSkinnable().setCursor(saveCursor);
					saveCursor = null;
					getSkinnable().requestLayout();
				}
				dragDetected = false;
			}

			/*
			 ** if the contents need repositioning, and there's is no touch event
			 * in progress, then start the repositioning.
			 */
			if ((posY > getSkinnable().getVmax() || posY < getSkinnable().getVmin() ||
					posX > getSkinnable().getHmax() || posX < getSkinnable().getHmin()) && !touchDetected) {
				startContentsToViewport();
			}
		});
		viewRect.setOnMouseDragged(e -> {
			if (IS_TOUCH_SUPPORTED) {
				startSBReleasedAnimation();
			}
			/*
			 ** for mobile-touch we allow drag, even if not pannagle
			 */
			if (getSkinnable().isPannable() || IS_TOUCH_SUPPORTED) {
				double deltaX = pressX - e.getX();
				double deltaY = pressY - e.getY();
				/*
				 ** we only drag if not all of the content is visible.
				 */
				if (hsb.getVisibleAmount() > 0.0 && hsb.getVisibleAmount() < hsb.getMax()) {
					if (Math.abs(deltaX) > PAN_THRESHOLD) {
						if (isReverseNodeOrientation()) {
							deltaX = -deltaX;
						}
						double newHVal = (ohvalue + deltaX / (nodeWidth - viewRect.getWidth()) * (hsb.getMax() - hsb.getMin()));
						if (!IS_TOUCH_SUPPORTED) {
							if (newHVal > hsb.getMax()) {
								newHVal = hsb.getMax();
							} else if (newHVal < hsb.getMin()) {
								newHVal = hsb.getMin();
							}
							hsb.setValue(newHVal);
						} else {
							hsb.setValue(newHVal);
						}
					}
				}
				/*
				 ** we only drag if not all of the content is visible.
				 */
				if (vsb.getVisibleAmount() > 0.0 && vsb.getVisibleAmount() < vsb.getMax()) {
					if (Math.abs(deltaY) > PAN_THRESHOLD) {
						double newVVal = (ovvalue + deltaY / (nodeHeight - viewRect.getHeight()) * (vsb.getMax() - vsb.getMin()));
						if (!IS_TOUCH_SUPPORTED) {
							if (newVVal > vsb.getMax()) {
								newVVal = vsb.getMax();
							} else if (newVVal < vsb.getMin()) {
								newVVal = vsb.getMin();
							}
							vsb.setValue(newVVal);
						} else {
							vsb.setValue(newVVal);
						}
					}
				}
			}
			/*
			 ** we need to consume drag events, as we don't want the scrollpane
			 * itself to be dragged on every mouse click
			 */
			e.consume();
		});

		/*
		 ** don't allow the ScrollBar to handle the ScrollEvent, In a ScrollPane
		 * a vertical scroll should scroll on the vertical only, whereas in a
		 * horizontal ScrollBar it can scroll horizontally.
		 */
		// block the event from being passed down to children
		final EventDispatcher blockEventDispatcher = (event, tail) -> event;
		// block ScrollEvent from being passed down to scrollbar's skin
		final EventDispatcher oldHsbEventDispatcher = hsb.getEventDispatcher();
		hsb.setEventDispatcher((event, tail) -> {
			if (event.getEventType() == ScrollEvent.SCROLL &&
					!((ScrollEvent) event).isDirect()) {
				tail = tail.prepend(blockEventDispatcher);
				tail = tail.prepend(oldHsbEventDispatcher);
				return tail.dispatchEvent(event);
			}
			return oldHsbEventDispatcher.dispatchEvent(event, tail);
		});
		// block ScrollEvent from being passed down to scrollbar's skin
		final EventDispatcher oldVsbEventDispatcher = vsb.getEventDispatcher();
		vsb.setEventDispatcher((event, tail) -> {
			if (event.getEventType() == ScrollEvent.SCROLL &&
					!((ScrollEvent) event).isDirect()) {
				tail = tail.prepend(blockEventDispatcher);
				tail = tail.prepend(oldVsbEventDispatcher);
				return tail.dispatchEvent(event);
			}
			return oldVsbEventDispatcher.dispatchEvent(event, tail);
		});

		/*
		 * listen for ScrollEvents over the whole of the ScrollPane area, the
		 * above dispatcher having removed the ScrollBars scroll event handling.
		 *
		 * Note that we use viewRect here, rather than setting the eventHandler
		 * on the ScrollPane itself. This is for RT-31582, and effectively
		 * allows for us to prioritise handling (and consuming) the event
		 * internally, before it is made available to users listening to events
		 * on the control. This is consistent with the VirtualFlow-based
		 * controls.
		 */
		viewRect.addEventHandler(ScrollEvent.SCROLL, event -> {
			if (IS_TOUCH_SUPPORTED) {
				startSBReleasedAnimation();
			}
			/*
			 ** if we're completely visible then do nothing.... we only consume
			 * an event that we've used.
			 */
			if (vsb.getVisibleAmount() < vsb.getMax()) {
				double vRange = getSkinnable().getVmax() - getSkinnable().getVmin();
				double vPixelValue;
				if (nodeHeight > 0.0) {
					vPixelValue = vRange / nodeHeight;
				} else {
					vPixelValue = 0.0;
				}
				double newValue = vsb.getValue() + (-event.getDeltaY()) * vPixelValue;
				if (!IS_TOUCH_SUPPORTED) {
					if ((event.getDeltaY() > 0.0 && vsb.getValue() > vsb.getMin()) ||
							(event.getDeltaY() < 0.0 && vsb.getValue() < vsb.getMax())) {
						vsb.setValue(newValue);
						event.consume();
					}
				} else {
					/*
					 ** if there is a repositioning in progress then we only set
					 * the value for 'real' events
					 */
					if (!(((ScrollEvent) event).isInertia()) || (((ScrollEvent) event).isInertia()) && (contentsToViewTimeline == null || contentsToViewTimeline.getStatus() == Status.STOPPED)) {
						vsb.setValue(newValue);
						if ((newValue > vsb.getMax() || newValue < vsb.getMin()) && (!mouseDown && !touchDetected)) {
							startContentsToViewport();
						}
						event.consume();
					}
				}
			}

			if (hsb.getVisibleAmount() < hsb.getMax()) {
				double hRange = getSkinnable().getHmax() - getSkinnable().getHmin();
				double hPixelValue;
				if (nodeWidth > 0.0) {
					hPixelValue = hRange / nodeWidth;
				} else {
					hPixelValue = 0.0;
				}

				double newValue = hsb.getValue() + (-event.getDeltaX()) * hPixelValue;
				if (!IS_TOUCH_SUPPORTED) {
					if ((event.getDeltaX() > 0.0 && hsb.getValue() > hsb.getMin()) ||
							(event.getDeltaX() < 0.0 && hsb.getValue() < hsb.getMax())) {
						hsb.setValue(newValue);
						event.consume();
					}
				} else {
					/*
					 ** if there is a repositioning in progress then we only set
					 * the value for 'real' events
					 */
					if (!(((ScrollEvent) event).isInertia()) || (((ScrollEvent) event).isInertia()) && (contentsToViewTimeline == null || contentsToViewTimeline.getStatus() == Status.STOPPED)) {
						hsb.setValue(newValue);

						if ((newValue > hsb.getMax() || newValue < hsb.getMin()) && (!mouseDown && !touchDetected)) {
							startContentsToViewport();
						}
						event.consume();
					}
				}
			}
		});

		/*
		 ** there are certain animations that need to know if the touch is
		 ** happening.....
		 */
		getSkinnable().addEventHandler(TouchEvent.TOUCH_PRESSED, e -> {
			touchDetected = true;
			startSBReleasedAnimation();
			e.consume();
		});

		getSkinnable().addEventHandler(TouchEvent.TOUCH_RELEASED, e -> {
			touchDetected = false;
			e.consume();
		});

		// ScrollPanes do not block all MouseEvents by default, unlike most
		// other UI Controls.
		consumeMouseEvents(false);

		// update skin initial state to match control (see RT-35554)
		hsb.setValue(control.getHvalue());
		vsb.setValue(control.getVvalue());
	}

	@Override
	protected void handleControlPropertyChanged(String p) {
		super.handleControlPropertyChanged(p);
		if ("NODE".equals(p)) {
			if (scrollNode != getSkinnable().getContent()) {
				if (scrollNode != null) {
					scrollNode.layoutBoundsProperty().removeListener(nodeListener);
					scrollNode.layoutBoundsProperty().removeListener(boundsChangeListener);
					viewContent.getChildren().remove(scrollNode);
				}
				scrollNode = getSkinnable().getContent();
				if (scrollNode != null) {
					nodeWidth = snapSize(scrollNode.getLayoutBounds().getWidth());
					nodeHeight = snapSize(scrollNode.getLayoutBounds().getHeight());
					viewContent.getChildren().setAll(scrollNode);
					scrollNode.layoutBoundsProperty().addListener(nodeListener);
					scrollNode.layoutBoundsProperty().addListener(boundsChangeListener);
				}
			}
			getSkinnable().requestLayout();
		} else if ("FIT_TO_WIDTH".equals(p) || "FIT_TO_HEIGHT".equals(p)) {
			getSkinnable().requestLayout();
			viewRect.requestLayout();
		} else if ("HBAR_POLICY".equals(p) || "VBAR_POLICY".equals(p)) {
			// change might affect pref size, so requestLayout on control
			getSkinnable().requestLayout();
		} else if ("HVALUE".equals(p)) {
			hsb.setValue(getSkinnable().getHvalue());
		} else if ("HMAX".equals(p)) {
			hsb.setMax(getSkinnable().getHmax());
		} else if ("HMIN".equals(p)) {
			hsb.setMin(getSkinnable().getHmin());
		} else if ("VVALUE".equals(p)) {
			vsb.setValue(getSkinnable().getVvalue());
		} else if ("VMAX".equals(p)) {
			vsb.setMax(getSkinnable().getVmax());
		} else if ("VMIN".equals(p)) {
			vsb.setMin(getSkinnable().getVmin());
		} else if ("VIEWPORT_SIZE_HINT".equals(p)) {
			// change affects pref size, so requestLayout on control
			getSkinnable().requestLayout();
		}
	}

	void scrollBoundsIntoView(Bounds b) {
		double dx = 0.0;
		double dy = 0.0;
		if (b.getMaxX() > contentWidth) {
			dx = b.getMinX() - snappedLeftInset();
		}
		if (b.getMinX() < snappedLeftInset()) {
			dx = b.getMaxX() - contentWidth - snappedLeftInset();
		}
		if (b.getMaxY() > snappedTopInset() + contentHeight) {
			dy = b.getMinY() - snappedTopInset();
		}
		if (b.getMinY() < snappedTopInset()) {
			dy = b.getMaxY() - contentHeight - snappedTopInset();
		}
		// We want to move contentPanel's layoutX,Y by (dx,dy).
		// But to do this we have to set the scrollbars' values appropriately.

		if (dx != 0) {
			double sdx = dx * (hsb.getMax() - hsb.getMin()) / (nodeWidth - contentWidth);
			// Adjust back for some amount so that the Node border is not too
			// close to view border
			sdx += -1 * Math.signum(sdx) * hsb.getUnitIncrement() / 5; // This
																		// accounts
																		// to 2%
																		// of
																		// view
																		// width
			hsb.setValue(hsb.getValue() + sdx);
			getSkinnable().requestLayout();
		}
		if (dy != 0) {
			double sdy = dy * (vsb.getMax() - vsb.getMin()) / (nodeHeight - contentHeight);
			// Adjust back for some amount so that the Node border is not too
			// close to view border
			sdy += -1 * Math.signum(sdy) * vsb.getUnitIncrement() / 5; // This
																		// accounts
																		// to 2%
																		// of
																		// view
																		// height
			vsb.setValue(vsb.getValue() + sdy);
			getSkinnable().requestLayout();
		}

	}

	/*
	 ** auto-scroll so node is within (0,0),(contentWidth,contentHeight)
	 */
	@Override
	public void onTraverse(Node n, Bounds b) {
		scrollBoundsIntoView(b);
	}

	public void hsbIncrement() {
		if (hsb != null)
			hsb.increment();
	}

	public void hsbDecrement() {
		if (hsb != null)
			hsb.decrement();
	}

	// TODO: add page increment and decrement
	public void hsbPageIncrement() {
		if (hsb != null)
			hsb.increment();
	}

	// TODO: add page increment and decrement
	public void hsbPageDecrement() {
		if (hsb != null)
			hsb.decrement();
	}

	public void vsbIncrement() {
		if (vsb != null)
			vsb.increment();
	}

	public void vsbDecrement() {
		if (vsb != null)
			vsb.decrement();
	}

	// TODO: add page increment and decrement
	public void vsbPageIncrement() {
		if (vsb != null)
			vsb.increment();
	}

	// TODO: add page increment and decrement
	public void vsbPageDecrement() {
		if (vsb != null)
			vsb.decrement();
	}

	/***************************************************************************
	 * * Layout * *
	 **************************************************************************/

	@Override
	protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
		final ScrollPane sp = getSkinnable();

		double vsbWidth = computeVsbSizeHint(sp);
		double minWidth = vsbWidth + snappedLeftInset() + snappedRightInset();

		if (sp.getPrefViewportWidth() > 0) {
			return (sp.getPrefViewportWidth() + minWidth);
		} else if (sp.getContent() != null) {
			return (sp.getContent().prefWidth(height) + minWidth);
		} else {
			return Math.max(minWidth, DEFAULT_PREF_SIZE);
		}
	}

	@Override
	protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
		final ScrollPane sp = getSkinnable();

		double hsbHeight = computeHsbSizeHint(sp);
		double minHeight = hsbHeight + snappedTopInset() + snappedBottomInset();

		if (sp.getPrefViewportHeight() > 0) {
			return (sp.getPrefViewportHeight() + minHeight);
		} else if (sp.getContent() != null) {
			return (sp.getContent().prefHeight(width) + minHeight);
		} else {
			return Math.max(minHeight, DEFAULT_PREF_SIZE);
		}
	}

	@Override
	protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
		final ScrollPane sp = getSkinnable();

		double vsbWidth = computeVsbSizeHint(sp);
		double minWidth = vsbWidth + snappedLeftInset() + snappedRightInset();

		if (sp.getMinViewportWidth() > 0) {
			return (sp.getMinViewportWidth() + minWidth);
		} else {
			double w = corner.minWidth(-1);
			return (w > 0) ? (3 * w) : (DEFAULT_MIN_SIZE);
		}

	}

	@Override
	protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
		final ScrollPane sp = getSkinnable();

		double hsbHeight = computeHsbSizeHint(sp);
		double minHeight = hsbHeight + snappedTopInset() + snappedBottomInset();

		if (sp.getMinViewportHeight() > 0) {
			return (sp.getMinViewportHeight() + minHeight);
		} else {
			double h = corner.minHeight(-1);
			return (h > 0) ? (3 * h) : (DEFAULT_MIN_SIZE);
		}
	}

	/**
	 * Computes the size that should be reserved for horizontal scrollbar in
	 * size hints (min/pref height)
	 */
	private double computeHsbSizeHint(ScrollPane sp) {
		return ((sp.getHbarPolicy() == ScrollBarPolicy.ALWAYS) ||
				(sp.getHbarPolicy() == ScrollBarPolicy.AS_NEEDED && (sp.getPrefViewportHeight() > 0 || sp.getMinViewportHeight() > 0)))
						? hsb.prefHeight(ScrollBar.USE_COMPUTED_SIZE)
						: 0;
	}

	/**
	 * Computes the size that should be reserved for vertical scrollbar in size
	 * hints (min/pref width)
	 */
	private double computeVsbSizeHint(ScrollPane sp) {
		return ((sp.getVbarPolicy() == ScrollBarPolicy.ALWAYS) ||
				(sp.getVbarPolicy() == ScrollBarPolicy.AS_NEEDED && (sp.getPrefViewportWidth() > 0
						|| sp.getMinViewportWidth() > 0)))
								? vsb.prefWidth(ScrollBar.USE_COMPUTED_SIZE)
								: 0;
	}

	@Override
	protected void layoutChildren(final double x, final double y,
			final double w, final double h) {
		final ScrollPane control = getSkinnable();
		final Insets padding = control.getPadding();
		final double rightPadding = snapSize(padding.getRight());
		final double leftPadding = snapSize(padding.getLeft());
		final double topPadding = snapSize(padding.getTop());
		final double bottomPadding = snapSize(padding.getBottom());

		vsb.setMin(control.getVmin());
		vsb.setMax(control.getVmax());

		// should only do this on css setup
		hsb.setMin(control.getHmin());
		hsb.setMax(control.getHmax());

		contentWidth = w;
		contentHeight = h;

		/*
		 ** we want the scrollbars to go right to the border
		 */
		double hsbWidth = 0;
		double vsbHeight = 0;

		computeScrollNodeSize(contentWidth, contentHeight);
		computeScrollBarSize();

		for (int i = 0; i < 2; ++i) {
			vsbvis = determineVerticalSBVisible();
			hsbvis = determineHorizontalSBVisible();

			if (vsbvis && !IS_TOUCH_SUPPORTED) {
				contentWidth = w - vsbWidth;
			}
			hsbWidth = w + leftPadding + rightPadding - (vsbvis ? vsbWidth : 0);
			if (hsbvis && !IS_TOUCH_SUPPORTED) {
				contentHeight = h - hsbHeight;
			}
			vsbHeight = h + topPadding + bottomPadding - (hsbvis ? hsbHeight : 0);
		}

		if (scrollNode != null && scrollNode.isResizable()) {
			// maybe adjust size now that scrollbars may take up space
			if (vsbvis && hsbvis) {
				// adjust just once to accommodate
				computeScrollNodeSize(contentWidth, contentHeight);

			} else if (hsbvis && !vsbvis) {
				computeScrollNodeSize(contentWidth, contentHeight);
				vsbvis = determineVerticalSBVisible();
				if (vsbvis) {
					// now both are visible
					contentWidth -= vsbWidth;
					hsbWidth -= vsbWidth;
					computeScrollNodeSize(contentWidth, contentHeight);
				}
			} else if (vsbvis && !hsbvis) {
				computeScrollNodeSize(contentWidth, contentHeight);
				hsbvis = determineHorizontalSBVisible();
				if (hsbvis) {
					// now both are visible
					contentHeight -= hsbHeight;
					vsbHeight -= hsbHeight;
					computeScrollNodeSize(contentWidth, contentHeight);
				}
			}
		}

		// figure out the content area that is to be filled
		double cx = snappedLeftInset() - leftPadding;
		double cy = snappedTopInset() - topPadding;

		vsb.setVisible(vsbvis);
		if (vsbvis) {
			/*
			 ** round up position of ScrollBar, round down it's size.
			 **
			 ** Positioning the ScrollBar The Padding should go between the
			 * content and the edge, otherwise changes in padding move the
			 * ScrollBar, and could in extreme cases size the ScrollBar to
			 * become unusable. The -1, +1 plus one bit : If padding in => 1
			 * then we allow one pixel to appear as the outside border of the
			 * Scrollbar, and the rest on the inside. If padding is < 1 then we
			 * just stick to the edge.
			 */
			vsb.resizeRelocate(snappedLeftInset() + w - vsbWidth + (rightPadding < 1 ? 0 : rightPadding - 1),
					cy, vsbWidth, vsbHeight);
		}
		updateVerticalSB();

		hsb.setVisible(hsbvis);
		if (hsbvis) {
			/*
			 ** round up position of ScrollBar, round down it's size.
			 **
			 ** Positioning the ScrollBar The Padding should go between the
			 * content and the edge, otherwise changes in padding move the
			 * ScrollBar, and could in extreme cases size the ScrollBar to
			 * become unusable. The -1, +1 plus one bit : If padding in => 1
			 * then we allow one pixel to appear as the outside border of the
			 * Scrollbar, and the rest on the inside. If padding is < 1 then we
			 * just stick to the edge.
			 */
			hsb.resizeRelocate(cx, snappedTopInset() + h - hsbHeight + (bottomPadding < 1 ? 0 : bottomPadding - 1),
					hsbWidth, hsbHeight);
		}
		updateHorizontalSB();

		viewRect.resizeRelocate(snappedLeftInset(), snappedTopInset(), snapSize(contentWidth), snapSize(contentHeight));
		resetClip();

		if (vsbvis && hsbvis) {
			corner.setVisible(true);
			double cornerWidth = vsbWidth;
			double cornerHeight = hsbHeight;
			corner.resizeRelocate(snapPosition(vsb.getLayoutX()), snapPosition(hsb.getLayoutY()), snapSize(cornerWidth), snapSize(cornerHeight));
		} else {
			corner.setVisible(false);
		}
		control.setViewportBounds(new BoundingBox(snapPosition(viewContent.getLayoutX()), snapPosition(viewContent.getLayoutY()), snapSize(contentWidth), snapSize(contentHeight)));
	}

	private void computeScrollNodeSize(double contentWidth, double contentHeight) {
		if (scrollNode != null) {
			if (scrollNode.isResizable()) {
				ScrollPane control = getSkinnable();
				Orientation bias = scrollNode.getContentBias();
				if (bias == null) {
					nodeWidth = snapSize(boundedSize(control.isFitToWidth() ? contentWidth : scrollNode.prefWidth(-1),
							scrollNode.minWidth(-1), scrollNode.maxWidth(-1)));
					nodeHeight = snapSize(boundedSize(control.isFitToHeight() ? contentHeight : scrollNode.prefHeight(-1),
							scrollNode.minHeight(-1), scrollNode.maxHeight(-1)));

				} else if (bias == Orientation.HORIZONTAL) {
					nodeWidth = snapSize(boundedSize(control.isFitToWidth() ? contentWidth : scrollNode.prefWidth(-1),
							scrollNode.minWidth(-1), scrollNode.maxWidth(-1)));
					nodeHeight = snapSize(boundedSize(control.isFitToHeight() ? contentHeight : scrollNode.prefHeight(nodeWidth),
							scrollNode.minHeight(nodeWidth), scrollNode.maxHeight(nodeWidth)));

				} else { // bias == VERTICAL
					nodeHeight = snapSize(boundedSize(control.isFitToHeight() ? contentHeight : scrollNode.prefHeight(-1),
							scrollNode.minHeight(-1), scrollNode.maxHeight(-1)));
					nodeWidth = snapSize(boundedSize(control.isFitToWidth() ? contentWidth : scrollNode.prefWidth(nodeHeight),
							scrollNode.minWidth(nodeHeight), scrollNode.maxWidth(nodeHeight)));
				}

			} else {
				nodeWidth = snapSize(scrollNode.getLayoutBounds().getWidth());
				nodeHeight = snapSize(scrollNode.getLayoutBounds().getHeight());
			}
			nodeSizeInvalid = false;
		}
	}

	private boolean isReverseNodeOrientation() {
		return (scrollNode != null &&
				getSkinnable().getEffectiveNodeOrientation() != scrollNode.getEffectiveNodeOrientation());
	}

	private boolean determineHorizontalSBVisible() {
		final ScrollPane sp = getSkinnable();

		if (IS_TOUCH_SUPPORTED) {
			return (tempVisibility && (nodeWidth > contentWidth));
		} else {
			// RT-17395: ScrollBarPolicy might be null. If so, treat it as
			// "AS_NEEDED", which is the default
			ScrollBarPolicy hbarPolicy = sp.getHbarPolicy();
			return (ScrollBarPolicy.NEVER == hbarPolicy) ? false
					: ((ScrollBarPolicy.ALWAYS == hbarPolicy) ? true : ((sp.isFitToWidth() && scrollNode != null ? scrollNode.isResizable() : false) ? (nodeWidth > contentWidth && scrollNode.minWidth(-1) > contentWidth) : (nodeWidth > contentWidth)));
		}
	}

	private boolean determineVerticalSBVisible() {
		final ScrollPane sp = getSkinnable();

		if (IS_TOUCH_SUPPORTED) {
			return (tempVisibility && (nodeHeight > contentHeight));
		} else {
			// RT-17395: ScrollBarPolicy might be null. If so, treat it as
			// "AS_NEEDED", which is the default
			ScrollBarPolicy vbarPolicy = sp.getVbarPolicy();
			return (ScrollBarPolicy.NEVER == vbarPolicy) ? false
					: ((ScrollBarPolicy.ALWAYS == vbarPolicy) ? true : ((sp.isFitToHeight() && scrollNode != null ? scrollNode.isResizable() : false) ? (nodeHeight > contentHeight && scrollNode.minHeight(-1) > contentHeight) : (nodeHeight > contentHeight)));
		}
	}

	private void computeScrollBarSize() {
		vsbWidth = snapSize(vsb.prefWidth(-1));
		if (vsbWidth == 0) {
			// println("*** WARNING ScrollPaneSkin: can't get scroll bar width,
			// using {DEFAULT_SB_BREADTH}");
			if (IS_TOUCH_SUPPORTED) {
				vsbWidth = DEFAULT_EMBEDDED_SB_BREADTH;
			} else {
				vsbWidth = DEFAULT_SB_BREADTH;
			}
		}
		hsbHeight = snapSize(hsb.prefHeight(-1));
		if (hsbHeight == 0) {
			// println("*** WARNING ScrollPaneSkin: can't get scroll bar height,
			// using {DEFAULT_SB_BREADTH}");
			if (IS_TOUCH_SUPPORTED) {
				hsbHeight = DEFAULT_EMBEDDED_SB_BREADTH;
			} else {
				hsbHeight = DEFAULT_SB_BREADTH;
			}
		}
	}

	private void updateHorizontalSB() {
		double contentRatio = nodeWidth * (hsb.getMax() - hsb.getMin());
		if (contentRatio > 0.0) {
			hsb.setVisibleAmount(contentWidth / contentRatio);
			hsb.setBlockIncrement(0.9 * hsb.getVisibleAmount());
			hsb.setUnitIncrement(0.1 * hsb.getVisibleAmount());
		} else {
			hsb.setVisibleAmount(0.0);
			hsb.setBlockIncrement(0.0);
			hsb.setUnitIncrement(0.0);
		}

		if (hsb.isVisible()) {
			updatePosX();
		} else {
			if (nodeWidth > contentWidth) {
				updatePosX();
			} else {
				viewContent.setLayoutX(0);
			}
		}
	}

	private void updateVerticalSB() {
		double contentRatio = nodeHeight * (vsb.getMax() - vsb.getMin());
		if (contentRatio > 0.0) {
			vsb.setVisibleAmount(contentHeight / contentRatio);
			vsb.setBlockIncrement(0.9 * vsb.getVisibleAmount());
			vsb.setUnitIncrement(0.1 * vsb.getVisibleAmount());
		} else {
			vsb.setVisibleAmount(0.0);
			vsb.setBlockIncrement(0.0);
			vsb.setUnitIncrement(0.0);
		}

		if (vsb.isVisible()) {
			updatePosY();
		} else {
			if (nodeHeight > contentHeight) {
				updatePosY();
			} else {
				viewContent.setLayoutY(0);
			}
		}
	}

	private double updatePosX() {
		final ScrollPane sp = getSkinnable();
		double x = isReverseNodeOrientation() ? (hsb.getMax() - (posX - hsb.getMin())) : posX;
		double minX = Math.min((-x / (hsb.getMax() - hsb.getMin()) * (nodeWidth - contentWidth)), 0);
		viewContent.setLayoutX(snapPosition(minX));
		if (!sp.hvalueProperty().isBound())
			sp.setHvalue(Utils.clamp(sp.getHmin(), posX, sp.getHmax()));
		return posX;
	}

	private double updatePosY() {
		final ScrollPane sp = getSkinnable();
		double minY = Math.min((-posY / (vsb.getMax() - vsb.getMin()) * (nodeHeight - contentHeight)), 0);
		viewContent.setLayoutY(snapPosition(minY));
		if (!sp.vvalueProperty().isBound())
			sp.setVvalue(Utils.clamp(sp.getVmin(), posY, sp.getVmax()));
		return posY;
	}

	private void resetClip() {
		clipRect.setWidth(snapSize(contentWidth));
		clipRect.setHeight(snapSize(contentHeight));
	}

	Timeline sbTouchTimeline;
	KeyFrame sbTouchKF1;
	KeyFrame sbTouchKF2;
	Timeline contentsToViewTimeline;
	KeyFrame contentsToViewKF1;
	KeyFrame contentsToViewKF2;
	KeyFrame contentsToViewKF3;

	private boolean tempVisibility;

	protected void startSBReleasedAnimation() {
		if (sbTouchTimeline == null) {
			/*
			 ** timeline to leave the scrollbars visible for a short while after
			 * a scroll/drag
			 */
			sbTouchTimeline = new Timeline();
			sbTouchKF1 = new KeyFrame(Duration.millis(0), event -> {
				tempVisibility = true;
				if (touchDetected == true || mouseDown == true) {
					sbTouchTimeline.playFromStart();
				}
			});

			sbTouchKF2 = new KeyFrame(Duration.millis(1000), event -> {
				tempVisibility = false;
				getSkinnable().requestLayout();
			});
			sbTouchTimeline.getKeyFrames().addAll(sbTouchKF1, sbTouchKF2);
		}
		sbTouchTimeline.playFromStart();
	}

	protected void startContentsToViewport() {
		double newPosX = posX;
		double newPosY = posY;

		setContentPosX(posX);
		setContentPosY(posY);

		if (posY > getSkinnable().getVmax()) {
			newPosY = getSkinnable().getVmax();
		} else if (posY < getSkinnable().getVmin()) {
			newPosY = getSkinnable().getVmin();
		}

		if (posX > getSkinnable().getHmax()) {
			newPosX = getSkinnable().getHmax();
		} else if (posX < getSkinnable().getHmin()) {
			newPosX = getSkinnable().getHmin();
		}

		if (!IS_TOUCH_SUPPORTED) {
			startSBReleasedAnimation();
		}

		/*
		 ** timeline to return the contents of the scrollpane to the viewport
		 */
		if (contentsToViewTimeline != null) {
			contentsToViewTimeline.stop();
		}
		contentsToViewTimeline = new Timeline();
		/*
		 ** short pause before animation starts
		 */
		contentsToViewKF1 = new KeyFrame(Duration.millis(50));
		/*
		 ** reposition
		 */
		contentsToViewKF2 = new KeyFrame(Duration.millis(150), event -> {
			getSkinnable().requestLayout();
		},
				new KeyValue(contentPosX, newPosX),
				new KeyValue(contentPosY, newPosY));
		/*
		 ** block out 'aftershocks', but real events will still reactivate
		 */
		contentsToViewKF3 = new KeyFrame(Duration.millis(1500));
		contentsToViewTimeline.getKeyFrames().addAll(contentsToViewKF1, contentsToViewKF2, contentsToViewKF3);
		contentsToViewTimeline.playFromStart();
	}

	private DoubleProperty contentPosX;

	private void setContentPosX(double value) {
		contentPosXProperty().set(value);
	}

	private double getContentPosX() {
		return contentPosX == null ? 0.0 : contentPosX.get();
	}

	private DoubleProperty contentPosXProperty() {
		if (contentPosX == null) {
			contentPosX = new DoublePropertyBase() {
				@Override
				protected void invalidated() {
					hsb.setValue(getContentPosX());
					getSkinnable().requestLayout();
				}

				@Override
				public Object getBean() {
					return OnlyScrollPaneSkin.this;
				}

				@Override
				public String getName() {
					return "contentPosX";
				}
			};
		}
		return contentPosX;
	}

	private DoubleProperty contentPosY;

	private void setContentPosY(double value) {
		contentPosYProperty().set(value);
	}

	private double getContentPosY() {
		return contentPosY == null ? 0.0 : contentPosY.get();
	}

	private DoubleProperty contentPosYProperty() {
		if (contentPosY == null) {
			contentPosY = new DoublePropertyBase() {
				@Override
				protected void invalidated() {
					vsb.setValue(getContentPosY());
					getSkinnable().requestLayout();
				}

				@Override
				public Object getBean() {
					return OnlyScrollPaneSkin.this;
				}

				@Override
				public String getName() {
					return "contentPosY";
				}
			};
		}
		return contentPosY;
	}

	@Override
	protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
		switch (attribute) {
		case VERTICAL_SCROLLBAR:
			return vsb;
		case HORIZONTAL_SCROLLBAR:
			return hsb;
		default:
			return super.queryAccessibleAttribute(attribute, parameters);
		}
	}
}
